Micron Document
NexusPi Git Node


Displaying Raw • Download

flash.py v1.0.17 (7b711813) Text, 42.95 KB

T8b949e#!/usr/bin/env python3
T8b949e"""
RNodeTHV4 Flash Utility

Flash the RNodeTHV4 boundary node firmware to a Heltec WiFi LoRa 32 V3 or V4.
No PlatformIO required — just Python 3 and a USB cable.

By default, downloads the latest firmware from GitHub Releases (if newer than
the local cache) and flashes the app partition only, preserving bootloader,
partition table, NVS, and EEPROM settings.

Usage:
# Update firmware — V4 (default)
python flash.py

# Update firmware — V3
python flash.py --board v3

# Flash a specific release version
python flash.py --release v1.0.12

# Full flash with merged binary (overwrites everything)
python flash.py --full

# Flash a specific file (auto-detects merged vs app-only)
python flash.py --file firmware.bin

# Specify serial port manually
python flash.py --port /dev/ttyACM0

# Skip online check — use cached/local firmware only
python flash.py --offline

# Just build the merged binary (for GitHub Releases)
python flash.py --merge-only
"""

Tff7b72import T7ee787argparse
Tff7b72import T7ee787glob
Tff7b72import T7ee787hashlib
Tff7b72import T7ee787os
Tff7b72import T7ee787platform
Tff7b72import T7ee787shutil
Tff7b72import T7ee787subprocess
Tff7b72import T7ee787sys
Tff7b72import T7ee787time

T8b949e# ── Configuration ──────────────────────────────────────────────────────────────

Te6edf3VERSION Tff7b72= Ta5d6ff"Ta5d6ff1.0.17Ta5d6ff"
Te6edf3CHIP Tff7b72= Ta5d6ff"Ta5d6ffesp32s3Ta5d6ff"
Te6edf3FLASH_MODE Tff7b72= Ta5d6ff"Ta5d6ffqioTa5d6ff"
Te6edf3FLASH_FREQ Tff7b72= Ta5d6ff"Ta5d6ff80mTa5d6ff"
Te6edf3GITHUB_REPO Tff7b72= Ta5d6ff"Ta5d6ffjrl290/RNodeTHV4Ta5d6ff"

T8b949e# Flash addresses for ESP32-S3 Arduino framework
Te6edf3BOOTLOADER_ADDR Tff7b72= T79c0ff0x0000
Te6edf3PARTITIONS_ADDR Tff7b72= T79c0ff0x8000
Te6edf3BOOT_APP0_ADDR Tff7b72= T79c0ff0xe000
Te6edf3APP_ADDR Tff7b72= T79c0ff0x10000

T8b949e# ── Board profiles ─────────────────────────────────────────────────────────────
T8b949e# Each board defines its PIO env, flash size, baud rate, firmware binary name,
T8b949e# and merged binary name.

Te6edf3BOARD_PROFILES Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4: Tb4b4b4{
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffHeltec WiFi LoRa 32 V4Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffheltec_V4_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff.pio/build/heltec_V4_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnode_firmware_heltec32v4_boundary.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnodethv4_firmware.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff16MBTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff921600Ta5d6ff"Tb4b4b4,
Tb4b4b4}Tb4b4b4,
Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4: Tb4b4b4{
Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffHeltec WiFi LoRa 32 V3Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffheltec_V3_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff.pio/build/heltec_V3_boundaryTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnode_firmware_heltec32v3.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffrnodethv3_firmware.binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff8MBTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ff460800Ta5d6ff"Tb4b4b4,
Tb4b4b4}Tb4b4b4,
Tb4b4b4}
Te6edf3DEFAULT_BOARD Tff7b72= Ta5d6ff"Ta5d6ffv4Ta5d6ff"

T8b949e# Active board profile (set in main() from --board arg)
Te6edf3_board Tff7b72= Tff7b72None

Tff7b72def Td2a8ffboard_profileTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3_board Tff7b72or Te6edf3DEFAULT_BOARDTb4b4b4]

Tff7b72def Td2a8ffBUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffbuild_dirTa5d6ff"Tb4b4b4]

Tff7b72def Td2a8ffBOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)

Tff7b72def Td2a8ffPARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)

Tff7b72def Td2a8ffFIRMWARE_BINTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3BUILD_DIRTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6fffirmware_binTa5d6ff"Tb4b4b4]Tb4b4b4)

Tff7b72def Td2a8ffFLASH_SIZETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4]

Tff7b72def Td2a8ffBAUD_RATETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffbaud_rateTa5d6ff"Tb4b4b4]

Tff7b72def Td2a8ffMERGED_FILENAMETb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4]

Tff7b72def Td2a8ffPIO_ENVTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3board_profileTb4b4b4(Tb4b4b4)Tb4b4b4[Ta5d6ff"Ta5d6ffpio_envTa5d6ff"Tb4b4b4]

T8b949e# ESP32 partition table magic bytes (first two bytes of a partition table entry)
Te6edf3PARTITION_TABLE_MAGIC Tff7b72= Ta5d6ffbTa5d6ff'Tffea00\xaaTffea00\x50Ta5d6ff'
Te6edf3PARTITION_TABLE_SIZE Tff7b72= T79c0ff0xC00 T8b949e# 3072 bytes


Tff7b72def Td2a8ffis_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
T8b949e"""Check whether a firmware file is a merged binary (contains bootloader +
partition table) or an app-only binary.

Returns True for merged, False for app-only.
"""
Tff7b72tryTb4b4b4:
Te6edf3size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)
Tff7b72if Te6edf3size Tff7b72> T79c0ff0x8002Tb4b4b4:
Tff7b72with Tffa657openTb4b4b4(Te6edf3firmware_pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3fTff7b72.Td2a8ffseekTb4b4b4(T79c0ff0x8000Tb4b4b4)
Tff7b72return Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(T79c0ff2Tb4b4b4) Tff7b72== Te6edf3PARTITION_TABLE_MAGIC
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72return Tff7b72False


Tff7b72def Td2a8ffextract_app_from_mergedTb4b4b4(Te6edf3merged_pathTb4b4b4)Tb4b4b4:
T8b949e"""Extract the app-only portion from a merged binary.

A merged binary starts at 0x0000 and includes bootloader, partition table,
boot_app0, and the app firmware. The region between the partition table
(0x8000-0x8BFF) and boot_app0 (0xE000) contains the NVS partition
(0x9000-0xDFFF) which is filled with 0xFF padding by esptool merge_bin.
Flashing a merged binary therefore wipes all saved settings.

This function extracts bytes from APP_ADDR (0x10000) to the end of the
file, producing an app-only binary that can be flashed at 0x10000 without
touching NVS/EEPROM.

Returns the path to the extracted app-only binary, or None on failure.
"""
Tff7b72tryTb4b4b4:
Te6edf3file_size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3merged_pathTb4b4b4)
Tff7b72if Te6edf3file_size Tff7b72<Tff7b72= Te6edf3APP_ADDRTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Warning: Merged binary too small (Tffd700{Te6edf3file_sizeTffd700}Ta5d6ff bytes) to contain app data.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72None

Tff7b72with Tffa657openTb4b4b4(Te6edf3merged_pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3fTff7b72.Td2a8ffseekTb4b4b4(Te6edf3APP_ADDRTb4b4b4)
Te6edf3app_data Tff7b72= Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)

Tff7b72if Tff7b72not Te6edf3app_dataTb4b4b4:
Tff7b72return Tff7b72None

Te6edf3baseTb4b4b4, Te6edf3ext Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffsplitextTb4b4b4(Te6edf3merged_pathTb4b4b4)
Te6edf3app_path Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff_appTffd700{Te6edf3extTffd700}Ta5d6ff"
Tff7b72with Tffa657openTb4b4b4(Te6edf3app_pathTb4b4b4, Ta5d6ff"Ta5d6ffwbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3fTff7b72.Td2a8ffwriteTb4b4b4(Te6edf3app_dataTb4b4b4)

Tff7b72return Te6edf3app_path
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Warning: Could not extract app from merged binary: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72None


Tff7b72def Td2a8ff_find_in_platformio_or_releaseTb4b4b4(Te6edf3build_pathTb4b4b4, Te6edf3release_nameTb4b4b4)Tb4b4b4:
T8b949e"""Find a file in the PlatformIO build output or the bundled Release/ dir."""
T8b949e# 1. PlatformIO build output
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3build_pathTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3build_path

T8b949e# 2. Bundled in Release/
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Te6edf3release_nameTb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3bundled

Tff7b72return Tff7b72None

T8b949e# Forward-compatible aliases (these are now functions, not constants)
Tff7b72def Td2a8ff_bootloader_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ff_partitions_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)

Tff7b72def Td2a8ff_firmware_binTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)


Tff7b72def Td2a8fffind_boot_app0Tb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find boot_app0.bin from PlatformIO framework packages.

Handles versioned package directories (e.g. framework-arduinoespressif32@3.20009.0).
"""
Te6edf3pio_dir Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(Ta5d6ff"Ta5d6ff~/.platformio/packagesTa5d6ff"Tb4b4b4)

T8b949e# Try exact name first
Te6edf3exact Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3pio_dirTb4b4b4, Ta5d6ff"Ta5d6ffframework-arduinoespressif32Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fftoolsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3exactTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3exact

T8b949e# Try versioned directories
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisdirTb4b4b4(Te6edf3pio_dirTb4b4b4)Tb4b4b4:
Tff7b72for Te6edf3name Tff7b72in Tffa657sortedTb4b4b4(Te6edf3osTff7b72.Td2a8fflistdirTb4b4b4(Te6edf3pio_dirTb4b4b4)Tb4b4b4, Te6edf3reverseTff7b72=Tff7b72TrueTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3nameTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffframework-arduinoespressif32Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3candidate Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3pio_dirTb4b4b4, Te6edf3nameTb4b4b4, Ta5d6ff"Ta5d6fftoolsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3candidateTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3candidate

T8b949e# Bundled fallback
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3bundled

Tff7b72return Tff7b72None


Tff7b72def Td2a8fffind_bootloaderTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find bootloader.bin from PlatformIO build output or Release/ bundle."""
Tff7b72return Te6edf3_find_in_platformio_or_releaseTb4b4b4(Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)


Tff7b72def Td2a8fffind_partitionsTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find partitions.bin from PlatformIO build output or Release/ bundle."""
Tff7b72return Te6edf3_find_in_platformio_or_releaseTb4b4b4(Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)


Te6edf3BOOT_APP0_BIN Tff7b72= Te6edf3find_boot_app0Tb4b4b4(Tb4b4b4)

T8b949e# ── Board auto-detection ───────────────────────────────────────────────────────

T8b949e# Map detected flash sizes to board keys
Te6edf3_FLASH_SIZE_TO_BOARD Tff7b72= Tb4b4b4{
Ta5d6ff"Ta5d6ff16MBTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff8MBTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4,
Tb4b4b4}

Tff7b72def Td2a8ffdetect_boardTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Auto-detect which Heltec board is connected by querying flash size.

Runs ``esptool.py flash_id`` and parses the output for:
- Detected flash size (16MB → V4, 8MB → V3)
- Chip type (ESP32-S3 expected)
- Features (PSRAM size, WiFi, BLE)

Returns a tuple (board_key, info_dict) on success, or (None, reason) on
failure. ``board_key`` is "v3" or "v4".
"""
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4, Ta5d6ff"Ta5d6ffflash_idTa5d6ff"Tb4b4b4]
Tff7b72tryTb4b4b4:
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4, Te6edf3timeoutTff7b72=T79c0ff15Tb4b4b4)
Tff7b72except Te6edf3subprocessTff7b72.Td2a8ffTimeoutExpiredTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6ff"Ta5d6ffesptool timed out (device not responding?)Ta5d6ff"
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Tffa657strTb4b4b4(Te6edf3eTb4b4b4)

Te6edf3output Tff7b72= Te6edf3resultTff7b72.Td2a8ffstdout Tff7b72+ Te6edf3resultTff7b72.Td2a8ffstderr
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffesptool flash_id failed:Tffea00\nTffd700{Te6edf3outputTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"

T8b949e# Parse key fields
Te6edf3info Tff7b72= Tb4b4b4{Tb4b4b4}
Tff7b72for Te6edf3line Tff7b72in Te6edf3outputTff7b72.Td2a8ffsplitlinesTb4b4b4(Tb4b4b4)Tb4b4b4:
Te6edf3line Tff7b72= Te6edf3lineTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffChip is Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffchipTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffChip is Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffFeatures:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6fffeaturesTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffFeatures:Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffDetected flash size:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4)Tb4b4b4[Tff7b72-T79c0ff1Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffMAC:Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffmacTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTff7b72.Td2a8ffsplitTb4b4b4(Ta5d6ff"Ta5d6ff:Ta5d6ff"Tb4b4b4)Tb4b4b4[Tff7b72-T79c0ff5Tb4b4b4:Tb4b4b4] T8b949e# last 5 colon-groups
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffmacTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffMAC:Ta5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Tff7b72elif Te6edf3lineTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffCrystal isTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3infoTb4b4b4[Ta5d6ff"Ta5d6ffcrystalTa5d6ff"Tb4b4b4] Tff7b72= Te6edf3lineTb4b4b4[Tffa657lenTb4b4b4(Ta5d6ff"Ta5d6ffCrystal isTa5d6ff"Tb4b4b4)Tb4b4b4:Tb4b4b4]Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)

Te6edf3flash_size Tff7b72= Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffflash_sizeTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3flash_sizeTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffCould not parse flash size from esptool output:Tffea00\nTffd700{Te6edf3outputTff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"

Te6edf3board_key Tff7b72= Te6edf3_FLASH_SIZE_TO_BOARDTff7b72.Td2a8ffgetTb4b4b4(Te6edf3flash_sizeTb4b4b4)
Tff7b72if Tff7b72not Te6edf3board_keyTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ffUnknown flash size Ta5d6ff'Tffd700{Te6edf3flash_sizeTffd700}Ta5d6ff'Ta5d6ff — expected 16MB (V4) or 8MB (V3).Tffea00\nTa5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ffUse --board v3 or --board v4 to specify manually.Ta5d6ff"
Tb4b4b4)

Tff7b72return Te6edf3board_keyTb4b4b4, Te6edf3info


T8b949e# ── Helpers ────────────────────────────────────────────────────────────────────

Tff7b72def Td2a8fffind_esptoolTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Find esptool.py — pip-installed, bundled, or PlatformIO's copy.

Prefer pip-installed esptool first (handles its own deps), then fall
back to the bundled script — but only if pyserial is importable in
the current Python interpreter.
"""
T8b949e# 1. pip-installed esptool (standalone executable, no dep issues)
Tff7b72if Te6edf3shutilTff7b72.Td2a8ffwhichTb4b4b4(Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tb4b4b4[Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4]
Tff7b72if Te6edf3shutilTff7b72.Td2a8ffwhichTb4b4b4(Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4)Tb4b4b4:
Tff7b72return Tb4b4b4[Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4]

T8b949e# Check if pyserial is available before using script-based esptool
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serial T8b949e# noqa: F401
Te6edf3has_pyserial Tff7b72= Tff7b72True
Tff7b72except Tf85149ImportErrorTb4b4b4:
Te6edf3has_pyserial Tff7b72= Tff7b72False

T8b949e# 2. Bundled in Release/
Te6edf3bundled Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffReleaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffesptoolTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffesptool.pyTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4) Tff7b72and Te6edf3has_pyserialTb4b4b4:
Tff7b72return Tb4b4b4[Te6edf3sysTff7b72.Td2a8ffexecutableTb4b4b4, Te6edf3bundledTb4b4b4]

T8b949e# 3. PlatformIO's esptool
Te6edf3pio_esptool Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffexpanduserTb4b4b4(
Ta5d6ff"Ta5d6ff~/.platformio/packages/tool-esptoolpy/esptool.pyTa5d6ff"
Tb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3pio_esptoolTb4b4b4) Tff7b72and Te6edf3has_pyserialTb4b4b4:
Tff7b72return Tb4b4b4[Te6edf3sysTff7b72.Td2a8ffexecutableTb4b4b4, Te6edf3pio_esptoolTb4b4b4]

T8b949e# 4. Bundled exists but pyserial is missing — tell the user
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3bundledTb4b4b4) Tff7b72and Tff7b72not Te6edf3has_pyserialTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffFound bundled esptool but pyserial is not installed.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install pyserialTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOr install the standalone esptool: pip install esptoolTa5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tff7b72return Tff7b72None


Tff7b72def Td2a8fffind_serial_portTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""List available serial ports and let the user choose."""
Te6edf3system Tff7b72= Te6edf3platformTff7b72.Td2a8ffsystemTb4b4b4(Tb4b4b4)

T8b949e# Gather ports from glob patterns
Tff7b72if Te6edf3system Tff7b72== Ta5d6ff"Ta5d6ffDarwinTa5d6ff"Tb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff/dev/cu.usbmodem*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/tty.usbmodem*Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff/dev/cu.usbserial*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/cu.SLAB*Ta5d6ff"Tb4b4b4]
Tff7b72elif Te6edf3system Tff7b72== Ta5d6ff"Ta5d6ffLinuxTa5d6ff"Tb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Ta5d6ff"Ta5d6ff/dev/ttyACM*Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff/dev/ttyUSB*Ta5d6ff"Tb4b4b4]
Tff7b72elseTb4b4b4:
Te6edf3patterns Tff7b72= Tb4b4b4[Tb4b4b4]

Te6edf3ports Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72for Te6edf3pattern Tff7b72in Te6edf3patternsTb4b4b4:
Te6edf3portsTff7b72.Td2a8ffextendTb4b4b4(Te6edf3globTff7b72.Td2a8ffglobTb4b4b4(Te6edf3patternTb4b4b4)Tb4b4b4)

T8b949e# Also try pyserial's port enumeration (works on all platforms including Windows)
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serialT7ee787.T7ee787toolsT7ee787.T7ee787list_ports
Tff7b72for Te6edf3port Tff7b72in Te6edf3serialTff7b72.Td2a8fftoolsTff7b72.Td2a8fflist_portsTff7b72.Td2a8ffcomportsTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3portTff7b72.Td2a8ffdevice Tff7b72not Tff7b72in Te6edf3portsTb4b4b4:
Te6edf3portsTff7b72.Td2a8ffappendTb4b4b4(Te6edf3portTff7b72.Td2a8ffdeviceTb4b4b4)
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tff7b72pass

T8b949e# Sort for consistent ordering
Te6edf3portsTff7b72.Td2a8ffsortTb4b4b4(Tb4b4b4)

Tff7b72if Tff7b72not Te6edf3portsTb4b4b4:
Tff7b72return Tff7b72None

Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffAvailable serial ports:Ta5d6ff"Tb4b4b4)
Tff7b72for Te6edf3iTb4b4b4, Te6edf3p Tff7b72in Tffa657enumerateTb4b4b4(Te6edf3portsTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff [Tffd700{Te6edf3iTff7b72+T79c0ff1Tffd700}Ta5d6ff] Tffd700{Te6edf3pTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)

Tff7b72while Tff7b72TrueTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffSelect port [1-Tffd700{Tffa657lenTb4b4b4(Te6edf3portsTb4b4b4)Tffd700}Ta5d6ff]: Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)
Te6edf3idx Tff7b72= Tffa657intTb4b4b4(Te6edf3choiceTb4b4b4) Tff7b72- T79c0ff1
Tff7b72if T79c0ff0 Tff7b72<Tff7b72= Te6edf3idx Tff7b72< Tffa657lenTb4b4b4(Te6edf3portsTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3portsTb4b4b4[Te6edf3idxTb4b4b4]
Tff7b72except Tb4b4b4(Tf85149ValueErrorTb4b4b4, Tf85149EOFErrorTb4b4b4)Tb4b4b4:
Tff7b72pass
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInvalid selection, try again.Ta5d6ff"Tb4b4b4)


Tff7b72def Td2a8ffsha256_fileTb4b4b4(Te6edf3pathTb4b4b4)Tb4b4b4:
T8b949e"""Compute SHA-256 hash of a file."""
Te6edf3h Tff7b72= Te6edf3hashlibTff7b72.Td2a8ffsha256Tb4b4b4(Tb4b4b4)
Tff7b72with Tffa657openTb4b4b4(Te6edf3pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Tff7b72for Te6edf3chunk Tff7b72in Tffa657iterTb4b4b4(Tff7b72lambdaTb4b4b4: Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(T79c0ff65536Tb4b4b4)Tb4b4b4, Ta5d6ffbTa5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3hTff7b72.Td2a8ffupdateTb4b4b4(Te6edf3chunkTb4b4b4)
Tff7b72return Te6edf3hTff7b72.Td2a8ffhexdigestTb4b4b4(Tb4b4b4)


Tff7b72def Td2a8ff_cache_dirTb4b4b4(Tb4b4b4)Tb4b4b4:
T8b949e"""Return the firmware cache directory (next to flash.py)."""
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffdirnameTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffabspathTb4b4b4(Tff7b72__file__Tb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ff.firmware_cacheTa5d6ff"Tb4b4b4)


Tff7b72def Td2a8ff_cache_meta_pathTb4b4b4(Te6edf3board_keyTb4b4b4)Tb4b4b4:
T8b949e"""Return path to the cache metadata JSON for a given board."""
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3_cache_dirTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_keyTb4b4b4, Ta5d6ff"Ta5d6ffmeta.jsonTa5d6ff"Tb4b4b4)


Tff7b72def Td2a8ff_cached_firmware_pathTb4b4b4(Te6edf3board_keyTb4b4b4)Tb4b4b4:
T8b949e"""Return path to the cached firmware binary for a given board."""
Tff7b72return Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3_cache_dirTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_keyTb4b4b4, Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3board_keyTb4b4b4]Tb4b4b4[Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4]Tb4b4b4)


Tff7b72def Td2a8ff_read_cache_metaTb4b4b4(Te6edf3board_keyTb4b4b4)Tb4b4b4:
T8b949e"""Read cache metadata, returning dict or None if not cached."""
Tff7b72import T7ee787json
Te6edf3meta_path Tff7b72= Te6edf3_cache_meta_pathTb4b4b4(Te6edf3board_keyTb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3meta_pathTb4b4b4)Tb4b4b4:
Tff7b72tryTb4b4b4:
Tff7b72with Tffa657openTb4b4b4(Te6edf3meta_pathTb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Tff7b72return Te6edf3jsonTff7b72.Td2a8ffloadTb4b4b4(Te6edf3fTb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72pass
Tff7b72return Tff7b72None


Tff7b72def Td2a8ff_write_cache_metaTb4b4b4(Te6edf3board_keyTb4b4b4, Te6edf3tagTb4b4b4, Te6edf3sha256Tb4b4b4)Tb4b4b4:
T8b949e"""Write cache metadata after a successful download."""
Tff7b72import T7ee787json
Te6edf3cache Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3_cache_dirTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_keyTb4b4b4)
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Te6edf3cacheTb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Te6edf3meta Tff7b72= Tb4b4b4{Ta5d6ff"Ta5d6fftagTa5d6ff"Tb4b4b4: Te6edf3tagTb4b4b4, Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Tb4b4b4: Te6edf3sha256Tb4b4b4}
Tff7b72with Tffa657openTb4b4b4(Te6edf3_cache_meta_pathTb4b4b4(Te6edf3board_keyTb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffwTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3jsonTff7b72.Td2a8ffdumpTb4b4b4(Te6edf3metaTb4b4b4, Te6edf3fTb4b4b4, Te6edf3indentTff7b72=T79c0ff2Tb4b4b4)


Tff7b72def Td2a8ff_parse_version_tagTb4b4b4(Te6edf3tagTb4b4b4)Tb4b4b4:
T8b949e"""Parse a version tag like 'v1.0.13' into a tuple (1, 0, 13) for comparison.
Returns None if the tag doesn't match the expected format."""
Tff7b72import T7ee787re
Te6edf3m Tff7b72= Te6edf3reTff7b72.Td2a8ffmatchTb4b4b4(Ta5d6ffrTa5d6ff"Ta5d6ffv?(Ta5d6ff\Ta5d6ffd+)Ta5d6ff\Ta5d6ff.(Ta5d6ff\Ta5d6ffd+)Ta5d6ff\Ta5d6ff.(Ta5d6ff\Ta5d6ffd+)Ta5d6ff"Tb4b4b4, Te6edf3tagTb4b4b4)
Tff7b72if Te6edf3mTb4b4b4:
Tff7b72return Tffa657tupleTb4b4b4(Tffa657intTb4b4b4(Te6edf3xTb4b4b4) Tff7b72for Te6edf3x Tff7b72in Te6edf3mTff7b72.Td2a8ffgroupsTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72return Tff7b72None


Tff7b72def Td2a8ff_fetch_release_infoTb4b4b4(Te6edf3tagTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Fetch release info from GitHub. If tag is None, fetches latest."""
Tff7b72tryTb4b4b4:
Tff7b72from T7ee787urllibT7ee787.T7ee787request Tff7b72import Te6edf3urlopenTb4b4b4, Te6edf3Request
Tff7b72import T7ee787json
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6ff"Ta5d6ffPython urllib not availableTa5d6ff"

Tff7b72if Te6edf3tagTb4b4b4:
T8b949e# Normalize: ensure tag starts with 'v'
Tff7b72if Tff7b72not Te6edf3tagTff7b72.Td2a8ffstartswithTb4b4b4(Ta5d6ff"Ta5d6ffvTa5d6ff"Tb4b4b4)Tb4b4b4:
Te6edf3tag Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffvTffd700{Te6edf3tagTffd700}Ta5d6ff"
Te6edf3api_url Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffhttps://api.github.com/repos/Tffd700{Te6edf3GITHUB_REPOTffd700}Ta5d6ff/releases/tags/Tffd700{Te6edf3tagTffd700}Ta5d6ff"
Tff7b72elseTb4b4b4:
Te6edf3api_url Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffhttps://api.github.com/repos/Tffd700{Te6edf3GITHUB_REPOTffd700}Ta5d6ff/releases/latestTa5d6ff"

Tff7b72tryTb4b4b4:
Te6edf3req Tff7b72= Te6edf3RequestTb4b4b4(Te6edf3api_urlTb4b4b4, Te6edf3headersTff7b72=Tb4b4b4{Ta5d6ff"Ta5d6ffAcceptTa5d6ff"Tb4b4b4: Ta5d6ff"Ta5d6ffapplication/vnd.github+jsonTa5d6ff"Tb4b4b4}Tb4b4b4)
Tff7b72with Te6edf3urlopenTb4b4b4(Te6edf3reqTb4b4b4, Te6edf3timeoutTff7b72=T79c0ff10Tb4b4b4) Tff7b72as Te6edf3respTb4b4b4:
Tff7b72return Te6edf3jsonTff7b72.Td2a8ffloadsTb4b4b4(Te6edf3respTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4, Tff7b72None
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Tffa657strTb4b4b4(Te6edf3eTb4b4b4)


Tff7b72def Td2a8fffetch_firmwareTb4b4b4(Te6edf3board_keyTb4b4b4, Te6edf3release_tagTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Fetch firmware from GitHub, using cache when possible.

Logic:
1. Query GitHub for the target release (latest or specific tag).
2. If the cached firmware matches that release tag, skip download.
3. Otherwise download the merged firmware binary and update cache.

Returns (firmware_path, release_tag) on success, (None, reason) on failure.
"""
Tff7b72from T7ee787urllibT7ee787.T7ee787request Tff7b72import Te6edf3urlretrieve

Te6edf3merged_name Tff7b72= Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3board_keyTb4b4b4]Tb4b4b4[Ta5d6ff"Ta5d6ffmerged_filenameTa5d6ff"Tb4b4b4]
Te6edf3cache_path Tff7b72= Te6edf3_cached_firmware_pathTb4b4b4(Te6edf3board_keyTb4b4b4)
Te6edf3cache_meta Tff7b72= Te6edf3_read_cache_metaTb4b4b4(Te6edf3board_keyTb4b4b4)

T8b949e# 1. Fetch release info
Te6edf3label Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ffrelease Tffd700{Te6edf3release_tagTffd700}Ta5d6ff" Tff7b72if Te6edf3release_tag Tff7b72else Ta5d6ff"Ta5d6fflatest releaseTa5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffChecking Tffd700{Te6edf3labelTffd700}Ta5d6ff from Tffd700{Te6edf3GITHUB_REPOTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Te6edf3releaseTb4b4b4, Te6edf3err Tff7b72= Te6edf3_fetch_release_infoTb4b4b4(Te6edf3release_tagTb4b4b4)
Tff7b72if Tff7b72not Te6edf3releaseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Could not reach GitHub: Tffd700{Te6edf3errTffd700}Ta5d6ff"Tb4b4b4)
T8b949e# Fall back to cache if available
Tff7b72if Te6edf3cache_meta Tff7b72and Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3cache_pathTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Using cached firmware: Tffd700{Te6edf3cache_metaTb4b4b4[Ta5d6ff'Ta5d6fftagTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Te6edf3cache_pathTb4b4b4, Te6edf3cache_metaTb4b4b4[Ta5d6ff"Ta5d6fftagTa5d6ff"Tb4b4b4]
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffNo cached firmware and GitHub unreachable: Tffd700{Te6edf3errTffd700}Ta5d6ff"

Te6edf3remote_tag Tff7b72= Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6fftag_nameTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffunknownTa5d6ff"Tb4b4b4)

T8b949e# 2. Check cache
Tff7b72if Te6edf3cache_meta Tff7b72and Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3cache_pathTb4b4b4)Tb4b4b4:
Te6edf3cached_tag Tff7b72= Te6edf3cache_metaTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6fftagTa5d6ff"Tb4b4b4)
Tff7b72if Te6edf3cached_tag Tff7b72== Te6edf3remote_tagTb4b4b4:
T8b949e# Verify file integrity
Te6edf3actual_sha Tff7b72= Te6edf3sha256_fileTb4b4b4(Te6edf3cache_pathTb4b4b4)
Tff7b72if Te6edf3actual_sha Tff7b72== Te6edf3cache_metaTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Tb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Cached firmware is up-to-date: Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Te6edf3cache_pathTb4b4b4, Te6edf3remote_tag
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Cache integrity mismatch — re-downloadingTa5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3cached_ver Tff7b72= Te6edf3_parse_version_tagTb4b4b4(Te6edf3cached_tagTb4b4b4) Tff7b72if Te6edf3cached_tag Tff7b72else Tff7b72None
Te6edf3remote_ver Tff7b72= Te6edf3_parse_version_tagTb4b4b4(Te6edf3remote_tagTb4b4b4)
Tff7b72if Te6edf3cached_ver Tff7b72and Te6edf3remote_ver Tff7b72and Te6edf3remote_ver Tff7b72> Te6edf3cached_verTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Newer version available: Tffd700{Te6edf3cached_tagTffd700}Ta5d6ff → Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elif Te6edf3cached_ver Tff7b72and Te6edf3remote_ver Tff7b72and Te6edf3remote_ver Tff7b72< Te6edf3cached_verTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Requested version Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff is older than cached Tffd700{Te6edf3cached_tagTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Version changed: Tffd700{Te6edf3cached_tagTffd700}Ta5d6ff → Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff"Tb4b4b4)

T8b949e# 3. Find the asset URL
Te6edf3asset_url Tff7b72= Tff7b72None
Tff7b72for Te6edf3asset Tff7b72in Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffassetsTa5d6ff"Tb4b4b4, Tb4b4b4[Tb4b4b4]Tb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3assetTb4b4b4[Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4] Tff7b72== Te6edf3merged_nameTb4b4b4:
Te6edf3asset_url Tff7b72= Te6edf3assetTb4b4b4[Ta5d6ff"Ta5d6ffbrowser_download_urlTa5d6ff"Tb4b4b4]
Tff7b72break

Tff7b72if Tff7b72not Te6edf3asset_urlTb4b4b4:
Te6edf3available Tff7b72= Tb4b4b4[Te6edf3aTb4b4b4[Ta5d6ff"Ta5d6ffnameTa5d6ff"Tb4b4b4] Tff7b72for Te6edf3a Tff7b72in Te6edf3releaseTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff"Ta5d6ffassetsTa5d6ff"Tb4b4b4, Tb4b4b4[Tb4b4b4]Tb4b4b4)Tb4b4b4]
Tff7b72return Tff7b72NoneTb4b4b4, Tb4b4b4(
Ta5d6fffTa5d6ff"Ta5d6ff'Tffd700{Te6edf3merged_nameTffd700}Ta5d6ff'Ta5d6ff not found in release Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff.Tffea00\nTa5d6ff"
Ta5d6fffTa5d6ff"Ta5d6ff Available assets: Tffd700{Te6edf3availableTffd700}Ta5d6ff"
Tb4b4b4)

T8b949e# 4. Download
Te6edf3osTff7b72.Td2a8ffmakedirsTb4b4b4(Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffjoinTb4b4b4(Te6edf3_cache_dirTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3board_keyTb4b4b4)Tb4b4b4, Te6edf3exist_okTff7b72=Tff7b72TrueTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Downloading Tffd700{Te6edf3remote_tagTffd700}Ta5d6ff / Tffd700{Te6edf3merged_nameTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3urlretrieveTb4b4b4(Te6edf3asset_urlTb4b4b4, Te6edf3cache_pathTb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tff7b72return Tff7b72NoneTb4b4b4, Ta5d6fffTa5d6ff"Ta5d6ffDownload failed: Tffd700{Te6edf3eTffd700}Ta5d6ff"

Te6edf3file_sha Tff7b72= Te6edf3sha256_fileTb4b4b4(Te6edf3cache_pathTb4b4b4)
Te6edf3file_size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3cache_pathTb4b4b4)
Te6edf3_write_cache_metaTb4b4b4(Te6edf3board_keyTb4b4b4, Te6edf3remote_tagTb4b4b4, Te6edf3file_shaTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Downloaded Tffd700{Te6edf3file_sizeTffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes SHA-256: Tffd700{Te6edf3file_shaTb4b4b4[Tb4b4b4:T79c0ff16Tb4b4b4]Tffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72return Te6edf3cache_pathTb4b4b4, Te6edf3remote_tag


Tff7b72def Td2a8ff_do_mergeTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3firmwareTb4b4b4)Tb4b4b4:
T8b949e"""Low-level merge: combine the four components into a single binary."""
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffMerging firmware components...Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Bootloader: Tffd700{Te6edf3bootloaderTffd700}Ta5d6ff @ 0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Partitions: Tffd700{Te6edf3partitionsTffd700}Ta5d6ff @ 0xTffd700{Te6edf3PARTITIONS_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff boot_app0: Tffd700{Te6edf3boot_app0Tffd700}Ta5d6ff @ 0xTffd700{Te6edf3BOOT_APP0_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Firmware: Tffd700{Te6edf3firmwareTffd700}Ta5d6ff @ 0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ff05xTffd700}Ta5d6ff"Tb4b4b4)

Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ffmerge_binTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--flash_modeTa5d6ff"Tb4b4b4, Te6edf3FLASH_MODETb4b4b4,
Ta5d6ff"Ta5d6ff--flash_freqTa5d6ff"Tb4b4b4, Te6edf3FLASH_FREQTb4b4b4,
Ta5d6ff"Ta5d6ff--flash_sizeTa5d6ff"Tb4b4b4, Te6edf3FLASH_SIZETb4b4b4(Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ff-oTa5d6ff"Tb4b4b4, Te6edf3output_pathTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3bootloaderTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3PARTITIONS_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3partitionsTb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOT_APP0_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3boot_app0Tb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4, Te6edf3firmwareTb4b4b4,
Tb4b4b4]

Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError merging: Tffd700{Te6edf3resultTff7b72.Td2a8ffstderrTffd700}Tffd700{Te6edf3resultTff7b72.Td2a8ffstdoutTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False

Te6edf3size Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3output_pathTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffMerged binary: Tffd700{Te6edf3output_pathTffd700}Ta5d6ff (Tffd700{Te6edf3sizeTffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes)Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffSHA-256: Tffd700{Te6edf3sha256_fileTb4b4b4(Te6edf3output_pathTb4b4b4)Tb4b4b4[Tb4b4b4:T79c0ff16Tb4b4b4]Tffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True


Tff7b72def Td2a8ffmerge_firmwareTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Merge bootloader + partitions + boot_app0 + app into a single binary.

Uses PlatformIO build output, falling back to bundled Release/ copies
for the boot components.
"""
Te6edf3bootloader Tff7b72= Te6edf3find_bootloaderTb4b4b4(Tb4b4b4)
Te6edf3partitions Tff7b72= Te6edf3find_partitionsTb4b4b4(Tb4b4b4)
Te6edf3boot_app0 Tff7b72= Te6edf3BOOT_APP0_BIN
Te6edf3firmware Tff7b72= Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)

Te6edf3missing Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3bootloaderTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffbootloaderTa5d6ff"Tb4b4b4, Te6edf3BOOTLOADER_BINTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3partitionsTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffpartitionsTa5d6ff"Tb4b4b4, Te6edf3PARTITIONS_BINTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3boot_app0Tb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6ffboot_app0Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff(not found)Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmwareTb4b4b4)Tb4b4b4:
Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Tb4b4b4(Ta5d6ff"Ta5d6fffirmwareTa5d6ff"Tb4b4b4, Te6edf3firmwareTb4b4b4)Tb4b4b4)

Tff7b72if Te6edf3missingTb4b4b4:
Tff7b72for Te6edf3nameTb4b4b4, Te6edf3path Tff7b72in Te6edf3missingTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: Tffd700{Te6edf3nameTffd700}Ta5d6ff not found: Tffd700{Te6edf3pathTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffRun Ta5d6ff'Ta5d6ffpio run -e Tffd700{Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff'Ta5d6ff to build first.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False

Tff7b72return Te6edf3_do_mergeTb4b4b4(Te6edf3output_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3firmwareTb4b4b4)


Tff7b72def Td2a8ffauto_merge_app_binaryTb4b4b4(Te6edf3app_binary_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
T8b949e"""Auto-merge an app-only binary with boot components for a full flash.

Finds bootloader, partitions, and boot_app0 from PlatformIO build output
or the bundled Release/ directory, then merges them with the supplied
app binary into a temporary merged file.

Returns the path to the merged binary on success, or None on failure.
"""
Te6edf3bootloader Tff7b72= Te6edf3find_bootloaderTb4b4b4(Tb4b4b4)
Te6edf3partitions Tff7b72= Te6edf3find_partitionsTb4b4b4(Tb4b4b4)
Te6edf3boot_app0 Tff7b72= Te6edf3BOOT_APP0_BIN

Te6edf3missing Tff7b72= Tb4b4b4[Tb4b4b4]
Tff7b72if Tff7b72not Te6edf3bootloaderTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffbootloader.binTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3partitionsTb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffpartitions.binTa5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3boot_app0Tb4b4b4: Te6edf3missingTff7b72.Td2a8ffappendTb4b4b4(Ta5d6ff"Ta5d6ffboot_app0.binTa5d6ff"Tb4b4b4)

Tff7b72if Te6edf3missingTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffCannot auto-merge: missing Tffd700{Ta5d6ff'Ta5d6ff, Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3missingTb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffPlace them in the Release/ folder alongside flash.py, orTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffbuild with PlatformIO: pio run -e Tffd700{Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72None

T8b949e# Create merged binary next to the app binary
Te6edf3baseTb4b4b4, Te6edf3ext Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffsplitextTb4b4b4(Te6edf3app_binary_pathTb4b4b4)
Te6edf3merged_path Tff7b72= Ta5d6fffTa5d6ff"Tffd700{Te6edf3baseTffd700}Ta5d6ff_mergedTffd700{Te6edf3extTffd700}Ta5d6ff"

Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAuto-merging app-only binary with boot components...Ta5d6ff"Tb4b4b4)
Tff7b72if Te6edf3_do_mergeTb4b4b4(Te6edf3merged_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3bootloaderTb4b4b4, Te6edf3partitionsTb4b4b4, Te6edf3boot_app0Tb4b4b4, Te6edf3app_binary_pathTb4b4b4)Tb4b4b4:
Tff7b72return Te6edf3merged_path
Tff7b72return Tff7b72None


Tff7b72def Td2a8ffread_device_partitionsTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Read the partition table from the connected device.

Uses esptool read_flash to read PARTITION_TABLE_SIZE bytes from
PARTITIONS_ADDR (0x8000).

Returns the raw bytes on success, or None on failure.
"""
Tff7b72import T7ee787tempfile
Tff7b72if Te6edf3baud Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3baud Tff7b72= Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)

Te6edf3tmp Tff7b72= Te6edf3tempfileTff7b72.Td2a8ffNamedTemporaryFileTb4b4b4(Te6edf3suffixTff7b72=Ta5d6ff"Ta5d6ff.binTa5d6ff"Tb4b4b4, Te6edf3deleteTff7b72=Tff7b72FalseTb4b4b4)
Te6edf3tmpTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6ffread_flashTa5d6ff"Tb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3PARTITIONS_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4,
Tffa657strTb4b4b4(Te6edf3PARTITION_TABLE_SIZETb4b4b4)Tb4b4b4,
Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4,
Tb4b4b4]
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4, Te6edf3timeoutTff7b72=T79c0ff30Tb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tff7b72return Tff7b72None
Tff7b72with Tffa657openTb4b4b4(Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Tff7b72return Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149ExceptionTb4b4b4:
Tff7b72return Tff7b72None
Tff7b72finallyTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3osTff7b72.Td2a8ffunlinkTb4b4b4(Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4)
Tff7b72except Tf85149OSErrorTb4b4b4:
Tff7b72pass


Tff7b72def Td2a8ffcheck_partition_tableTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Compare the device's partition table against the expected one.

Returns:
True — partition table matches (or no expected table to compare against)
False — partition table mismatch (device needs full flash)
"""
Te6edf3expected_path Tff7b72= Te6edf3find_partitionsTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3expected_pathTb4b4b4:
T8b949e# Can't check — no reference partition table available
Tff7b72return Tff7b72True

Tff7b72with Tffa657openTb4b4b4(Te6edf3expected_pathTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3expected Tff7b72= Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)

Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffChecking device partition table...Ta5d6ff"Tb4b4b4)
Te6edf3device_data Tff7b72= Te6edf3read_device_partitionsTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTb4b4b4)
Tff7b72if Te6edf3device_data Tff7b72is Tff7b72NoneTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Could not read partition table from deviceTa5d6ff"Tb4b4b4)
T8b949e# Can't verify — assume OK (user can always use --full)
Tff7b72return Tff7b72True

T8b949e# Compare only the meaningful portion (both should be PARTITION_TABLE_SIZE)
Tff7b72if Te6edf3device_dataTb4b4b4[Tb4b4b4:Tffa657lenTb4b4b4(Te6edf3expectedTb4b4b4)Tb4b4b4] Tff7b72== Te6edf3expectedTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Partition table OK ✓Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True

T8b949e# Check if device has any valid partition table at all
Tff7b72if Te6edf3device_dataTb4b4b4[Tb4b4b4:T79c0ff2Tb4b4b4] Tff7b72!= Te6edf3PARTITION_TABLE_MAGICTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff No valid partition table found on device (blank or corrupted)Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Partition table MISMATCH — device has a different layoutTa5d6ff"Tb4b4b4)

Tff7b72return Tff7b72False


Tff7b72def Td2a8ffcheck_app_on_deviceTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Check whether app firmware is present on the device.

Reads a small chunk from APP_ADDR (0x10000). If the region is all 0xFF
(erased flash), no app is present and the device needs a full flash.

Returns True if app firmware is detected, False if blank/absent.
"""
Tff7b72import T7ee787tempfile
Tff7b72if Te6edf3baud Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3baud Tff7b72= Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)

Te6edf3read_size Tff7b72= T79c0ff256 T8b949e# enough to distinguish blank from real firmware
Te6edf3tmp Tff7b72= Te6edf3tempfileTff7b72.Td2a8ffNamedTemporaryFileTb4b4b4(Te6edf3suffixTff7b72=Ta5d6ff"Ta5d6ff.binTa5d6ff"Tb4b4b4, Te6edf3deleteTff7b72=Tff7b72FalseTb4b4b4)
Te6edf3tmpTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6ffread_flashTa5d6ff"Tb4b4b4,
Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"Tb4b4b4,
Tffa657strTb4b4b4(Te6edf3read_sizeTb4b4b4)Tb4b4b4,
Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4,
Tb4b4b4]
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4, Te6edf3capture_outputTff7b72=Tff7b72TrueTb4b4b4, Te6edf3textTff7b72=Tff7b72TrueTb4b4b4, Te6edf3timeoutTff7b72=T79c0ff30Tb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Warning: Could not read app region from deviceTa5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True T8b949e# assume app exists if we can't check
Tff7b72with Tffa657openTb4b4b4(Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4, Ta5d6ff"Ta5d6ffrbTa5d6ff"Tb4b4b4) Tff7b72as Te6edf3fTb4b4b4:
Te6edf3data Tff7b72= Te6edf3fTff7b72.Td2a8ffreadTb4b4b4(Tb4b4b4)
T8b949e# All 0xFF means flash is blank — no app present
Tff7b72if Te6edf3data Tff7b72== Ta5d6ffbTa5d6ff'Tffea00\xffTa5d6ff' Tff7b72* Tffa657lenTb4b4b4(Te6edf3dataTb4b4b4)Tb4b4b4:
Tff7b72return Tff7b72False
Tff7b72return Tff7b72True
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Warning: App check failed: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True T8b949e# assume app exists if we can't check
Tff7b72finallyTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3osTff7b72.Td2a8ffunlinkTb4b4b4(Te6edf3tmpTff7b72.Td2a8ffnameTb4b4b4)
Tff7b72except Tf85149OSErrorTb4b4b4:
Tff7b72pass


Tff7b72def Td2a8ffreset_to_bootloaderTb4b4b4(Te6edf3portTb4b4b4)Tb4b4b4:
T8b949e"""Open serial port at 1200 baud to trigger ESP32-S3 USB bootloader reset.

Many ESP32-S3 boards with native USB will enter download mode when
the port is opened at 1200 baud with DTR toggled. This is useful
when the device is stuck or unresponsive to normal esptool connection.
"""
Tff7b72tryTb4b4b4:
Tff7b72import T7ee787serial
Tff7b72except Tf85149ImportErrorTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffError: pyserial is required for 1200 baud reset.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install pyserialTa5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False

Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffOpening Tffd700{Te6edf3portTffd700}Ta5d6ff at 1200 baud to trigger bootloader...Ta5d6ff"Tb4b4b4)
Tff7b72tryTb4b4b4:
Te6edf3ser Tff7b72= Te6edf3serialTff7b72.Td2a8ffSerialTb4b4b4(Te6edf3portTb4b4b4, T79c0ff1200Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72False
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.1Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72True
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff0.1Tb4b4b4)
Te6edf3serTff7b72.Td2a8ffdtr Tff7b72= Tff7b72False
Te6edf3serTff7b72.Td2a8ffcloseTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149Exception Tff7b72as Te6edf3eTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: Tffd700{Te6edf3eTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72False

Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffWaiting for device to re-enumerate in download mode...Ta5d6ff"Tb4b4b4)
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff3Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffDone. The device should now be in download mode.Ta5d6ff"Tb4b4b4)
Tff7b72return Tff7b72True


Tff7b72def Td2a8ffflash_firmwareTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTff7b72=Tff7b72NoneTb4b4b4)Tb4b4b4:
T8b949e"""Flash firmware to the device."""
Tff7b72if Te6edf3baud Tff7b72is Tff7b72NoneTb4b4b4:
Te6edf3baud Tff7b72= Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)
Te6edf3flash_size Tff7b72= Te6edf3FLASH_SIZETb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffFlashing Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff to Tffd700{Te6edf3portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Chip: Tffd700{Te6edf3CHIPTffd700}Ta5d6ff Baud: Tffd700{Te6edf3baudTffd700}Ta5d6ff Flash: Tffd700{Te6edf3flash_sizeTffd700}Tffea00\nTa5d6ff"Tb4b4b4)

T8b949e# Determine if this is a merged binary (flash at 0x0) or app-only (flash at 0x10000)
Te6edf3is_merged Tff7b72= Te6edf3is_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)

Tff7b72if Te6edf3is_mergedTb4b4b4:
Te6edf3flash_addr Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Detected: merged binary (partition table at 0x8000) -> flash at Tffd700{Te6edf3flash_addrTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3flash_addr Tff7b72= Ta5d6fffTa5d6ff"Ta5d6ff0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ffxTffd700}Ta5d6ff"
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Detected: app-only binary -> flash at Tffd700{Te6edf3flash_addrTffd700}Ta5d6ff"Tb4b4b4)

Te6edf3cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6ff--beforeTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffdefault_resetTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--afterTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffhard_resetTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffwrite_flashTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff-zTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ff--flash_modeTa5d6ff"Tb4b4b4, Te6edf3FLASH_MODETb4b4b4,
Ta5d6ff"Ta5d6ff--flash_freqTa5d6ff"Tb4b4b4, Te6edf3FLASH_FREQTb4b4b4,
Ta5d6ff"Ta5d6ff--flash_sizeTa5d6ff"Tb4b4b4, Te6edf3flash_sizeTb4b4b4,
Te6edf3flash_addrTb4b4b4, Te6edf3firmware_pathTb4b4b4,
Tb4b4b4]

Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffRunning: Ta5d6ff" Tff7b72+ Ta5d6ff"Ta5d6ff Ta5d6ff"Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3cmdTb4b4b4[Tff7b72-T79c0ff8Tb4b4b4:Tb4b4b4]Tb4b4b4)Tb4b4b4)
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3cmdTb4b4b4)
Tff7b72return Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72== T79c0ff0


T8b949e# ── Main ───────────────────────────────────────────────────────────────────────

Tff7b72def Td2a8ffmainTb4b4b4(Tb4b4b4)Tb4b4b4:
Tff7b72global Te6edf3_board
Te6edf3parser Tff7b72= Te6edf3argparseTff7b72.Td2a8ffArgumentParserTb4b4b4(
Te6edf3descriptionTff7b72=Ta5d6ff"Ta5d6ffRNodeTHV4 Flash Utility — flash boundary node firmware to Heltec V3/V4Ta5d6ff"Tb4b4b4,
Te6edf3formatter_classTff7b72=Te6edf3argparseTff7b72.Td2a8ffRawDescriptionHelpFormatterTb4b4b4,
Te6edf3epilogTff7b72=Ta5d6ff"""
Ta5d6ffExamples:
Ta5d6ff python flash.py # Download latest & app-only update (V4)
Ta5d6ff python flash.py --board v3 # Download latest & app-only update (V3)
Ta5d6ff python flash.py --release v1.0.12 # Flash a specific release version
Ta5d6ff python flash.py --full # Full flash with merged binary
Ta5d6ff python flash.py --offline # Use cached/local firmware only
Ta5d6ff python flash.py --file firmware.bin # Flash a specific file
Ta5d6ff python flash.py --merge-only # Build merged binary for release
Ta5d6ff python flash.py --port /dev/ttyACM0 # Specify serial port
Ta5d6ff python flash.py --erase --full # Erase flash, then full flash
Ta5d6ff Ta5d6ff"""Tb4b4b4,
Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--boardTa5d6ff"Tb4b4b4, Te6edf3choicesTff7b72=Tb4b4b4[Ta5d6ff"Ta5d6ffv3Ta5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffv4Ta5d6ff"Tb4b4b4]Tb4b4b4, Te6edf3defaultTff7b72=Tff7b72NoneTb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffTarget board: v3 (Heltec V3) or v4 (Heltec V4). Ta5d6ff"
Ta5d6ff"Ta5d6ffAuto-detected from connected device if omitted.Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--fileTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-fTa5d6ff"Tb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffPath to firmware binary to flashTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-pTa5d6ff"Tb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffSerial port (auto-detected if omitted)Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-bTa5d6ff"Tb4b4b4, Te6edf3defaultTff7b72=Tff7b72NoneTb4b4b4, Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffBaud rate (board-specific default)Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--releaseTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ff-rTa5d6ff"Tb4b4b4, Te6edf3defaultTff7b72=Tff7b72NoneTb4b4b4, Te6edf3metavarTff7b72=Ta5d6ff"Ta5d6ffTAGTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffFlash a specific release version (e.g. v1.0.12)Ta5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--offlineTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffSkip online check — use cached or local firmware onlyTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--merge-onlyTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffMerge PlatformIO build output into single binary, donTa5d6ff'Ta5d6fft flashTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--fullTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffFlash merged binary (bootloader + partitions + app) — overwrites everythingTa5d6ff"Tb4b4b4)
Te6edf3parserTff7b72.Td2a8ffadd_argumentTb4b4b4(Ta5d6ff"Ta5d6ff--eraseTa5d6ff"Tb4b4b4, Te6edf3actionTff7b72=Ta5d6ff"Ta5d6ffstore_trueTa5d6ff"Tb4b4b4,
Te6edf3helpTff7b72=Ta5d6ff"Ta5d6ffErase entire flash before writing (implies --full)Ta5d6ff"Tb4b4b4)

Te6edf3args Tff7b72= Te6edf3parserTff7b72.Td2a8ffparse_argsTb4b4b4(Tb4b4b4)

T8b949e# Find esptool early — needed for both auto-detect and flashing
Te6edf3esptool_cmd Tff7b72= Te6edf3find_esptoolTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3esptool_cmdTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffError: esptool not found!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffInstall it with: pip install esptoolTa5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

T8b949e# ── Board detection ─────────────────────────────────────────────────
Te6edf3detected_info Tff7b72= Tff7b72None
Te6edf3_early_port Tff7b72= Tff7b72None

Tff7b72if Te6edf3argsTff7b72.Td2a8ffboardTb4b4b4:
T8b949e# Explicit board — no detection needed
Te6edf3_board Tff7b72= Te6edf3argsTff7b72.Td2a8ffboard
Tff7b72elif Te6edf3argsTff7b72.Td2a8ffmerge_onlyTb4b4b4:
T8b949e# No device needed for merge — fall back to default
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff(No --board specified; defaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff for merge)Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Auto-detect from connected device
Te6edf3_early_port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3_early_portTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo serial port detected and no --board specified.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDefaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff. Specify with --board v3 or --board v4.Ta5d6ff"Tb4b4b4)
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffDetecting board on Tffd700{Te6edf3_early_portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Te6edf3board_keyTb4b4b4, Te6edf3info Tff7b72= Te6edf3detect_boardTb4b4b4(Te6edf3_early_portTb4b4b4, Te6edf3esptool_cmdTb4b4b4)
Tff7b72if Te6edf3board_keyTb4b4b4:
Te6edf3_board Tff7b72= Te6edf3board_key
Te6edf3detected_info Tff7b72= Te6edf3info
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Chip: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffchipTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Flash: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffflash_sizeTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Features: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6fffeaturesTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff MAC: Tffd700{Te6edf3infoTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6ffmacTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff → Detected: Tffd700{Te6edf3BOARD_PROFILESTb4b4b4[Te6edf3board_keyTb4b4b4]Tb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3reason Tff7b72= Te6edf3info T8b949e# info is the error reason when board_key is None
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Auto-detect failed: Tffd700{Te6edf3reasonTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Defaulting to Tffd700{Te6edf3DEFAULT_BOARDTffd700}Ta5d6ff. Specify with --board v3 or --board v4.Ta5d6ff"Tb4b4b4)
Te6edf3_board Tff7b72= Te6edf3DEFAULT_BOARD

Te6edf3baud Tff7b72= Te6edf3argsTff7b72.Td2a8ffbaud Tff7b72or Te6edf3BAUD_RATETb4b4b4(Tb4b4b4)
Te6edf3bp Tff7b72= Te6edf3board_profileTb4b4b4(Tb4b4b4)

Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╔══════════════════════════════════════════╗Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ RNodeTHV4 Flash Utility ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff║ Tffd700{Te6edf3bpTb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700:Ta5d6ff^40sTffd700}Ta5d6ff ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╚══════════════════════════════════════════╝Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing esptool: Tffd700{Ta5d6ff'Ta5d6ff Ta5d6ff'Tff7b72.Td2a8ffjoinTb4b4b4(Te6edf3esptool_cmdTb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)

T8b949e# --erase implies --full (after erase, device needs bootloader + partitions)
Tff7b72if Te6edf3argsTff7b72.Td2a8fferaseTb4b4b4:
Te6edf3argsTff7b72.Td2a8fffull Tff7b72= Tff7b72True

T8b949e# Determine firmware file
Te6edf3firmware_path Tff7b72= Tff7b72None
Te6edf3merged_fn Tff7b72= Te6edf3MERGED_FILENAMETb4b4b4(Tb4b4b4)
Te6edf3firmware_bin Tff7b72= Te6edf3FIRMWARE_BINTb4b4b4(Tb4b4b4)
Te6edf3pio_env Tff7b72= Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)

Tff7b72if Te6edf3argsTff7b72.Td2a8fffileTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3argsTff7b72.Td2a8fffile
Tff7b72if Tff7b72not Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffError: file not found: Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tff7b72elif Te6edf3argsTff7b72.Td2a8ffmerge_onlyTb4b4b4:
Tff7b72if Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffDone! Flash with: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file Tffd700{Te6edf3merged_fnTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72return

Tff7b72elif Te6edf3argsTff7b72.Td2a8fffull Tff7b72and Tff7b72not Te6edf3argsTff7b72.Td2a8ffrelease Tff7b72and Te6edf3argsTff7b72.Td2a8ffofflineTb4b4b4:
T8b949e# Full flash, offline: use local PIO build or existing merged binary
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_binTb4b4b4)Tb4b4b4:
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3build_time Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetmtimeTb4b4b4(Te6edf3firmware_binTb4b4b4)
Te6edf3merge_time Tff7b72= Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetmtimeTb4b4b4(Te6edf3merged_fnTb4b4b4)
Tff7b72if Te6edf3build_time Tff7b72> Te6edf3merge_timeTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffBuild output is newer than merged binary, re-merging...Ta5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffCreating merged binary from PlatformIO build output...Ta5d6ff"Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3merge_firmwareTb4b4b4(Te6edf3merged_fnTb4b4b4, Te6edf3esptool_cmdTb4b4b4)Tb4b4b4:
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tff7b72elif Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tff7b72elseTb4b4b4:
T8b949e# Try cache
Te6edf3cached Tff7b72= Te6edf3_cached_firmware_pathTb4b4b4(Te6edf3_boardTb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3cachedTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3cached
Te6edf3meta Tff7b72= Te6edf3_read_cache_metaTb4b4b4(Te6edf3_boardTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing cached firmware: Tffd700{Te6edf3metaTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6fftagTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4) Tff7b72if Te6edf3meta Tff7b72else Ta5d6ff'Ta5d6ff?Ta5d6ff'Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo firmware found for full flash!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOptions:Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 1. Build with PlatformIO first: pio run -e Tffd700{Te6edf3pio_envTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 2. Run without --offline to download from GitHubTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 3. Specify a file: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file <path>Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tff7b72elseTb4b4b4:
T8b949e# Default path: fetch from GitHub (unless --offline)
Tff7b72if Tff7b72not Te6edf3argsTff7b72.Td2a8ffofflineTb4b4b4:
Te6edf3fw_pathTb4b4b4, Te6edf3tag_or_err Tff7b72= Te6edf3fetch_firmwareTb4b4b4(Te6edf3_boardTb4b4b4, Te6edf3release_tagTff7b72=Te6edf3argsTff7b72.Td2a8ffreleaseTb4b4b4)
Tff7b72if Te6edf3fw_pathTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3fw_path
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ff Release: Tffd700{Te6edf3tag_or_errTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ff GitHub: Tffd700{Te6edf3tag_or_errTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Falling back to local firmware...Ta5d6ff"Tb4b4b4)

T8b949e# Fall back to local PIO build output or cache
Tff7b72if Tff7b72not Te6edf3firmware_pathTb4b4b4:
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3firmware_binTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3firmware_bin
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing local PlatformIO build: Tffd700{Te6edf3firmware_binTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Te6edf3cached Tff7b72= Te6edf3_cached_firmware_pathTb4b4b4(Te6edf3_boardTb4b4b4)
Tff7b72if Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3cachedTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3cached
Te6edf3meta Tff7b72= Te6edf3_read_cache_metaTb4b4b4(Te6edf3_boardTb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing cached firmware: Tffd700{Te6edf3metaTff7b72.Td2a8ffgetTb4b4b4(Ta5d6ff'Ta5d6fftagTa5d6ff'Tb4b4b4, Ta5d6ff'Ta5d6ff?Ta5d6ff'Tb4b4b4) Tff7b72if Te6edf3meta Tff7b72else Ta5d6ff'Ta5d6ff?Ta5d6ff'Tffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elif Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffisfileTb4b4b4(Te6edf3merged_fnTb4b4b4)Tb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged_fn
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing local merged binary: Tffd700{Te6edf3merged_fnTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffNo firmware found!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffOptions:Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 1. Build with PlatformIO first: pio run -e Tffd700{Te6edf3pio_envTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff 2. Specify a file: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --file <path>Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

T8b949e# ── Device checks & flash decision ──────────────────────────────────────
T8b949e#
T8b949e# Flow:
T8b949e# 1. --full or --erase on CLI → full_flash = True
T8b949e# 2. Check if app firmware exists on device → if not → full_flash = True
T8b949e# 3. Check partition table matches expected → if not → full_flash = True
T8b949e# 4. Ask user "Erase flash before writing?" → Y → full_flash = True
T8b949e# 5. full_flash → flash merged binary at 0x0000
T8b949e# 6. Otherwise → extract app from merged, flash at 0x10000

T8b949e# Reuse early-detected port, or find one now
Te6edf3port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3_early_port Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Tff7b72not Te6edf3portTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffError: No serial port detected!Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffConnect your Tffd700{Te6edf3bpTb4b4b4[Ta5d6ff'Ta5d6ffnameTa5d6ff'Tb4b4b4]Tffd700}Ta5d6ff via USB and try again,Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffor specify manually: python flash.py --board Tffd700{Te6edf3_boardTffd700}Ta5d6ff --port /dev/ttyACM0Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffSerial port: Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffFirmware: Tffd700{Te6edf3firmware_pathTffd700}Ta5d6ff (Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700:Ta5d6ff,Tffd700}Ta5d6ff bytes)Ta5d6ff"Tb4b4b4)

Te6edf3full_flash Tff7b72= Te6edf3argsTff7b72.Td2a8fffull Tff7b72or Te6edf3argsTff7b72.Td2a8fferase

Tff7b72if Tff7b72not Te6edf3full_flashTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffChecking device state...Ta5d6ff"Tb4b4b4)
Te6edf3has_app Tff7b72= Te6edf3check_app_on_deviceTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTb4b4b4)
Tff7b72if Tff7b72not Te6edf3has_appTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff No app firmware on device — full flash requiredTa5d6ff"Tb4b4b4)
Te6edf3full_flash Tff7b72= Tff7b72True

Tff7b72if Tff7b72not Te6edf3full_flashTb4b4b4:
Te6edf3pt_ok Tff7b72= Te6edf3check_partition_tableTb4b4b4(Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTb4b4b4)
Tff7b72if Tff7b72not Te6edf3pt_okTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff Partition table mismatch — full flash requiredTa5d6ff"Tb4b4b4)
Te6edf3full_flash Tff7b72= Tff7b72True

Tff7b72if Tff7b72not Te6edf3full_flashTb4b4b4:
Tff7b72tryTb4b4b4:
Te6edf3erase_choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffErase flash before writing? (wipes all settings) [y/N] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149EOFErrorTb4b4b4:
Te6edf3erase_choice Tff7b72= Ta5d6ff"Ta5d6ff"
Tff7b72if Te6edf3erase_choice Tff7b72== Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Te6edf3full_flash Tff7b72= Tff7b72True

T8b949e# ── Prepare firmware based on flash decision ────────────────────────────
Tff7b72if Te6edf3full_flashTb4b4b4:
T8b949e# Need the merged binary — ensure we have one
Tff7b72if Tff7b72not Te6edf3is_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffCreating merged binary for full flash...Ta5d6ff"Tb4b4b4)
Te6edf3merged Tff7b72= Te6edf3auto_merge_app_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3esptool_cmdTb4b4b4)
Tff7b72if Te6edf3mergedTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3merged
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff ERROR: Cannot create merged binary — missing boot components.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Build with PlatformIO first: pio run -e Tffd700{Te6edf3PIO_ENVTb4b4b4(Tb4b4b4)Tffd700}Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ff Full flash: Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffbasenameTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700}Ta5d6ff → 0xTffd700{Te6edf3BOOTLOADER_ADDRTffd700:Ta5d6ff04xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Size: Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700:Ta5d6ff,Tffd700}Ta5d6ff bytesTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff ⚠ This will overwrite all settings (NVS/EEPROM)Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
T8b949e# Extract app-only from merged binary to preserve settings
Tff7b72if Te6edf3is_merged_binaryTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tb4b4b4:
Te6edf3app_path Tff7b72= Te6edf3extract_app_from_mergedTb4b4b4(Te6edf3firmware_pathTb4b4b4)
Tff7b72if Te6edf3app_pathTb4b4b4:
Te6edf3firmware_path Tff7b72= Te6edf3app_path
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ff ERROR: Could not extract app from merged binary.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)

Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ff App-only update: Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffbasenameTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700}Ta5d6ff → 0xTffd700{Te6edf3APP_ADDRTffd700:Ta5d6ff05xTffd700}Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff Size: Tffd700{Te6edf3osTff7b72.Td2a8ffpathTff7b72.Td2a8ffgetsizeTb4b4b4(Te6edf3firmware_pathTb4b4b4)Tffd700:Ta5d6ff,Tffd700}Ta5d6ff bytesTa5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ff WiFi/boundary settings will be preservedTa5d6ff"Tb4b4b4)

T8b949e# ── Interactive options ─────────────────────────────────────────────────

T8b949e# Offer 1200 baud reset if device might be stuck
Tff7b72tryTb4b4b4:
Te6edf3reset_choice Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffReset device to download mode first? (try if device is stuck) [y/N] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72except Tf85149EOFErrorTb4b4b4:
Te6edf3reset_choice Tff7b72= Ta5d6ff"Ta5d6ff"
Tff7b72if Te6edf3reset_choice Tff7b72== Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Te6edf3reset_to_bootloaderTb4b4b4(Te6edf3portTb4b4b4)
T8b949e# Port may change after reset — re-scan
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffRe-scanning serial ports (port may have changed)...Ta5d6ff"Tb4b4b4)
Te6edf3new_port Tff7b72= Te6edf3argsTff7b72.Td2a8ffport Tff7b72or Te6edf3find_serial_portTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3new_portTb4b4b4:
Te6edf3port Tff7b72= Te6edf3new_port
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffUsing port: Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Ta5d6ffWarning: No ports found after reset. Continuing with Tffd700{Te6edf3portTffd700}Ta5d6ff"Tb4b4b4)

Te6edf3confirm Tff7b72= Tffa657inputTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffFlash firmware? [Y/n] Ta5d6ff"Tb4b4b4)Tff7b72.Td2a8ffstripTb4b4b4(Tb4b4b4)Tff7b72.Td2a8fflowerTb4b4b4(Tb4b4b4)
Tff7b72if Te6edf3confirm Tff7b72and Te6edf3confirm Tff7b72!= Ta5d6ff"Ta5d6ffyTa5d6ff"Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffAborted.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff0Tb4b4b4)

T8b949e# ── Erase flash (only when --erase was explicitly passed) ───────────────
Tff7b72if Te6edf3argsTff7b72.Td2a8fferaseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6fffTa5d6ff"Tffea00\nTa5d6ffErasing flash on Tffd700{Te6edf3portTffd700}Ta5d6ff...Ta5d6ff"Tb4b4b4)
Te6edf3erase_cmd Tff7b72= Te6edf3esptool_cmd Tff7b72+ Tb4b4b4[
Ta5d6ff"Ta5d6ff--chipTa5d6ff"Tb4b4b4, Te6edf3CHIPTb4b4b4,
Ta5d6ff"Ta5d6ff--portTa5d6ff"Tb4b4b4, Te6edf3portTb4b4b4,
Ta5d6ff"Ta5d6ff--baudTa5d6ff"Tb4b4b4, Te6edf3baudTb4b4b4,
Ta5d6ff"Ta5d6fferase_flashTa5d6ff"Tb4b4b4,
Tb4b4b4]
Te6edf3result Tff7b72= Te6edf3subprocessTff7b72.Td2a8ffrunTb4b4b4(Te6edf3erase_cmdTb4b4b4)
Tff7b72if Te6edf3resultTff7b72.Td2a8ffreturncode Tff7b72!= T79c0ff0Tb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffErase FAILED.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffFlash erased. Waiting for device to re-enumerate...Ta5d6ff"Tb4b4b4)
Te6edf3timeTff7b72.Td2a8ffsleepTb4b4b4(T79c0ff3Tb4b4b4)

Tff7b72if Te6edf3flash_firmwareTb4b4b4(Te6edf3firmware_pathTb4b4b4, Te6edf3portTb4b4b4, Te6edf3esptool_cmdTb4b4b4, Te6edf3baudTb4b4b4)Tb4b4b4:
Tffa657printTb4b4b4(Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╔══════════════════════════════════════════╗Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ Flash complete! ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ Device will reboot automatically. ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ On first boot, hold PRG > 5s to enter ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff║ the configuration portal. ║Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ff╚══════════════════════════════════════════╝Ta5d6ff"Tb4b4b4)
Tff7b72elseTb4b4b4:
Tffa657printTb4b4b4(Ta5d6ff"Tffea00\nTa5d6ffFlash FAILED. Check connection and try again.Ta5d6ff"Tb4b4b4)
Tffa657printTb4b4b4(Ta5d6ff"Ta5d6ffYou may need to hold BOOT while pressing RESET.Ta5d6ff"Tb4b4b4)
Te6edf3sysTff7b72.Td2a8ffexitTb4b4b4(T79c0ff1Tb4b4b4)


Tff7b72if Tff7b72__name__ Tff7b72== Ta5d6ff"Ta5d6ff__main__Ta5d6ff"Tb4b4b4:
Te6edf3mainTb4b4b4(Tb4b4b4)

Served by rngit 1.4.2 - Generated in 0.21s